<?php
/**
 * Hỗ trợ kỹ thuật template mark-replace
 *
 * Sử dụng vòng lặp
 * {loop persons}
    <tr>
        <td>{$C_COUNTER}</td> <!-- Biến mặc định đếm thứ tự -->
        <td>{$maso}</td>
        <td>{$hoten}</td>
    </tr>
    {/loop}
 *
 * Sử dụng if
 * {if logged|1} {# 1 giá trị mặc định nếu không thiết lập logged}
 *  Các html khi logged == 1/true
 * {else logged}
 *  các html khi logged == 0/false
 * {/if logged}
 *
 * Sử dụng include để chèn tập tin template khác vào
 * {include file} {# file không có phần mở rộng html}
 */
class VTemplate implements ArrayAccess
{
    /**
     * @var string Lưu trữ text đợc được từ file template để thay thế
     */
    private $_buffer;
    /**
     * @var array Lưu các biến dùng để thay thế
     */
    private $_variables;
    /**
     * @var array Danh sách các file Css
     */
    private $_css;
    /**
     * @var array Danh sách các file Javascript
     */
    private $_js;
    /**
     * Đường dẫn mặc định của file template khi khởi tạo Object
     * @var null
     */
    private $_file;

    /**
     * Khởi tạo Object
     * @param string $buffer
     * @param null $file
     */
    public function __construct($buffer = null, $file = null)
    {
        $this->_buffer = $buffer;
        $this->_variables = array();
        $this->_css = array();
        $this->_js = array();
        $this->_file = $file;

        if (!defined('ROOT_URL')) {
            define('ROOT_URL', 'localhost');
        }
        if (!defined('VIEW_DIR')) {
            define('VIEW_DIR', dirname(__FILE__));
        }
    }

    /**
     * Thêm/xóa css link file
     * @param $file string|array css file
     * @param bool $f true: thêm | false: xóa
     */
    public function css($file, $f = true)
    {
        if (is_array($file)) {
            foreach ($file as $fi) {
                $this->css($fi, $f);
            }
        } else {
            $exist =in_array($file, $this->_css);
            if ( $exist && !$f) {
                $this->_css = array_diff($this->_css, array($file));
            } elseif (!$exist && $f) {
                $this->_css[] = $file;
            }
        }
    }

    /**
     * Thêm/xóa javascript link file
     * @param $file string|array js file
     * @param bool $f true: thêm | false : xóa
     */
    public function js($file, $f = true)
    {
        if (is_array($file)) {
            foreach ($file as $fi) {
                $this->js($fi, $f);
            }
        } else {
            $exist =in_array($file, $this->_js);
            if ( $exist && !$f) {
                $this->_js = array_diff($this->_js, array($file));
            } elseif (!$exist && $f) {
                $this->_js[] = $file;
            }
        }
    }

    /**
     * Gán giá trị vào để thay thế trong template
     * @param $key string Chuỗi cần tìm trong template để thay thế
     * @param string $value giá trị để thay, có thể là giá trị đơn hoặc associate array
     */
    public function assign($key, $value = '')
    {
        if (is_array($key)) {
            $this->_variables = array_merge($this->_variables, $key);
        } elseif (is_object($key)) {
            $this->assign((array) $key);
        } else {
            $this->_variables[$key] = $value;
        }
    }

    /**
     * Cài đặt giao diện ArrayAccess
     * @param $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        return isset($this->_variables[$offset]);
    }

    /**
     * Cài đặt giao diện ArrayAccess
     * @param $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        if (!isset($this->_variables[$offset])) {
            $this->_variables[$offset] = '';
        }
        return $this->_variables[$offset];
    }

    /**
     * Cài đặt giao diện ArrayAccess
     * @param $offset
     * @param $value
     */
    public function offsetSet($offset, $value)
    {
        $this->_variables[$offset] = $value;
    }

    /**
     * Cài đặt giao diện ArrayAccess
     * @param $offset
     */
    public function offsetUnset($offset)
    {
        if (isset($this->_variables[$offset])) {
            unset($this->_variables[$offset]);
        }
    }

    /**
     * Thực hiện phân tích và thay thế
     * @param null $file nếu là path sẽ đọc nội dung file đó vào buffer
     * @return string
     */
    public function parse($file = null)
    {
        # load template content from file if be set & exist
        if ($file != null && file_exists($file)) {
            $this->_buffer = file_get_contents($file);
        } elseif ($this->_file != null && file_exists($this->_file)) {
            $this->_buffer = file_get_contents($this->_file);
        }

        # parse include statement
        $this->include_processing();

        # remove comments
        $this->removeComments();

        # add Css & Js files
        $this->addCssJs_processing();

        # parse if statement
        $pattern = '/{if (?P<var>\w*?)(?:\|(?P<default>\d))?}(?P<then>[\s\S]*?)(?:{else \1}(?P<else>[\s\S]*?))?{\/if \1}/mi';

        while (preg_match($pattern, $this->_buffer, $match)) {
            $varName = $match['var'];

            $newContent = isset($match['else']) ? $match['else'] : '';
            if ((isset($this->_variables[$varName]) && $this->_variables[$varName])
                || (!isset($this->_variables[$varName]) && $match['default'] == '1')) {
                $newContent = $match['then'];
            }
            $this->_buffer = str_replace($match[0], $newContent, $this->_buffer);
        }


        # parse loop statement
        $pattern = '/{loop (?P<var>\w+)}(?P<content>[\s\S]*?){\/loop \1}/mi';

        while (preg_match($pattern, $this->_buffer, $match)) {
            $varName = $match['var'];
            $content = $match['content'];
            $parsed = '';
            if (isset($this->_variables[$varName]) && is_array($this->_variables[$varName])) {
                $counter = 0;
                foreach ($this->_variables[$varName] as $item) {
                    $tmp_template = new VTemplate($content);
                    $tmp_template->assign($item);
                    $tmp_template->assign('C_COUNTER', ++$counter);
                    $parsed .= $tmp_template->parse();
                }
            }
            $this->_buffer = str_replace($match[0], $parsed, $this->_buffer);

        }


        // processing replacement
        $this->_variables['ROOT'] = ROOT_URL;
        foreach ($this->_variables as $key => $value) {
            if (is_object($value) && get_class($value) == __CLASS__) {
                $tmp = $value->parse();
                $this->css($value->_css);
                $this->js($value->_js);
                $value = $tmp;
            }
            $pattern = '/\{\$' . $key . '(?:(?:\|)(.*?))?\}/';
            if (preg_match($pattern, $this->_buffer)) {
                $this->_buffer = @preg_replace($pattern, $value, $this->_buffer);
            }
        }

        # default values
        $pattern = '/\{\$\w+\|(.*?)\}/';
        $this->_buffer = preg_replace($pattern, "$1", $this->_buffer);

        # add Css link
        $pattern = '/(?P<opentag>\<head\>)(?P<content>[\s\S]+?)(?P<closetag>\<\/head\>)/';
        $cssLinks = '';
        $jsSrc = '';
        foreach ($this->_css as $css) {
            $cssLinks .= "\r\n<link rel=\"stylesheet\" type=\"text/css\" href=\"{$css}\">";
        }
        foreach ($this->_js as $js) {
            $jsSrc .= "\r\n<script type=\"text/javascript\" src=\"{$js}\"></script>";
        }

        $this->_buffer = preg_replace($pattern, "$1$2$cssLinks$jsSrc\r\n$3", $this->_buffer);

        $root = str_replace('/', '\/', ROOT_URL);
        #echo "<p>$root</p>";
        // search for internal link
        $pattern = '/((?:href|src|action)=)"(?!http|' . $root .')\/?(.*?|)"/m';
        $this->_buffer = preg_replace($pattern, "$1" . '"' . ROOT_URL . "$2" . '"', $this->_buffer);

        return $this->_buffer;
    }

    public function include_processing()
    {
        $pattern = '/\{include ([\w\-\/]+)\}/';
        $counter = preg_match_all($pattern, $this->_buffer, $matches);
        for ($i = 0; $i < $counter; $i++) {
            $file = VIEWS_DIR . $matches[1][$i] . '.html';
            $val = '';
            if (file_exists($file)) {
                $val = file_get_contents($file);
            } else {
                throw new Exception("Include template failed! <{$file}>");
            }
            $this->_buffer = str_replace($matches[0][$i], $val, $this->_buffer);
        }

    }

    public function addCssJs_processing()
    {
        $pattern = '/\{(?P<type>css|js)\s(?P<file>.+?)\}/';
        $counter = preg_match_all($pattern, $this->_buffer, $matches);
        for ($i = 0; $i < $counter; $i++) {
            if ($matches['type'][$i] == 'css') {
                $this->css($matches['file'][$i]);
            } else {
                $this->js($matches['file'][$i]);
            }

            $this->_buffer = str_replace($matches[0][$i], null, $this->_buffer);
        }
    }

    public function removeComments()
    {
        $pattern = '/\{\#[\s\S]+?\}|\{comment\}[\s\S]+?\{\/comment\}/';
        $this->_buffer = preg_replace($pattern, null, $this->_buffer);
    }

    protected function json()
    {
        return json_encode($this->_variables);
    }

    public function display($file)
    {
        if (file_exists($file)) {
            echo $this->parse($file);
        } else {
            throw New Exception('Template file [' . $file . '] không tồn tại!');
        }
    }
}